﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Xml.Linq;
using System.Xml.Schema;



namespace Docentric.Examples.Data.PlainDataSets
{
	/// <summary>
	/// This class contains methods for converting a 'DataTable' object to an XML document that is suitable for data-binding. It can also retrieve a corresponding XSD that can be imported into
	/// a Docentric template. (This class will eventually become part of the Docentric toolkit in one of the future releases)
	/// </summary>
	public class DataTableToXmlConverter
	{
		// Constants
		private const string NAME_EXTENSION_ROW = "_Row";
		private const string NAME_EXTENSION_ROWS = "_Rows";

		// Fields
		private DataTable _dataTable;
		private Dictionary<string, string> _processedDataRelationsInDataTables = new Dictionary<string, string>();



		#region Constructor
		public DataTableToXmlConverter(DataTable dataTable)
		{
			_dataTable = dataTable;
		}
		#endregion



		#region XML Methods
		/// <summary>
		/// Serializes the data table into an XML fragment retaining the hierarchy in case the data table is part of a data set containing several interconnected data tables (via relationships).
		/// </summary>
		/// <returns>An XML fragment representing the serialized data table.</returns>
		public XElement GetXml()
		{
			// Create the root element.
			string rootElementName = GetDataTableOrColumnName(_dataTable.TableName);
			XElement dataTableXElement = new XElement(XName.Get(rootElementName + NAME_EXTENSION_ROWS));

			// Loop through all data table rows.
			foreach (DataRow dataRow in _dataTable.Rows)
			{
				// Create the element for the current data row.
				XElement dataRowXElement = new XElement(XName.Get(rootElementName + NAME_EXTENSION_ROW));
				dataTableXElement.Add(dataRowXElement);

				GetXml_FillDataRowXElement(dataRow, dataRowXElement);
			}

			// Return.
			return dataTableXElement;
		}


		/// <summary>
		/// Serializes the provided data row into an XML element and adds it to the parent XML element. This method runs recursively (it takes all relationships into account).
		/// </summary>
		/// <param name="dataRow"></param>
		/// <param name="parentXElement"></param>
		/// <returns></returns>
		private XElement GetXml_FillDataRowXElement(DataRow dataRow, XElement dataRowXElement)
		{
			// Loop through all data table columns.
			foreach (DataColumn dataColumn in dataRow.Table.Columns)
			{
				// Create the XML attribute for the current data field.
				XAttribute dataColumnXAttribute = new XAttribute(XName.Get(GetDataTableOrColumnName(dataColumn.ColumnName)), dataRow[dataColumn]);
				dataRowXElement.Add(dataColumnXAttribute);

				// Find and aliminate any '\0' char.
				if (dataColumnXAttribute.Value.Contains((char)0))
				{
					dataColumnXAttribute.Value = dataColumnXAttribute.Value.Replace("\0", "");
				}
			}

			// Parent Relations.
			foreach (DataRelation parentRelation in dataRow.Table.ParentRelations)
			{
				// Each relation can be used to build hierarchy for each table only once, otherwise the recursion never stops (stack overflow occures).
				if (_processedDataRelationsInDataTables.ContainsKey(parentRelation.RelationName) &&
					_processedDataRelationsInDataTables[parentRelation.RelationName] != dataRow.Table.TableName) continue;
				_processedDataRelationsInDataTables[parentRelation.RelationName] = dataRow.Table.TableName;

				// Hierarchy is supported only for the data relations with 1 column (parent and child).
				if (parentRelation.ParentColumns.Length > 1 || parentRelation.ChildColumns.Length > 1) continue;

				// Get name for the XML element representing the parent table row.
				string parentName = GetParentRelationXElementName(parentRelation);

				// Get the value in the parent table.
				DataRow parentDataRow = dataRow.GetParentRow(parentRelation);
				if (parentDataRow != null)
				{
					// Create an XML element for the parent row.
					XElement parentRelationXElement = new XElement(XName.Get(parentName));
					dataRowXElement.Add(parentRelationXElement);

					// Fill the 'parentRelationXElement'.
					GetXml_FillDataRowXElement(parentDataRow, parentRelationXElement);
				}
			}

			// Child Relations.
			foreach (DataRelation childRelation in dataRow.Table.ChildRelations)
			{
				// Each relation can be used to build hierarchy for each table only once, otherwise the recursion never stops (stack overflow occures).
				if (_processedDataRelationsInDataTables.ContainsKey(childRelation.RelationName) &&
					_processedDataRelationsInDataTables[childRelation.RelationName] != dataRow.Table.TableName) continue;
				_processedDataRelationsInDataTables[childRelation.RelationName] = dataRow.Table.TableName;

				// Hierarchy is supported only for the data relations with 1 column (parent and child).
				if (childRelation.ParentColumns.Length > 1 || childRelation.ChildColumns.Length > 1) continue;

				// Get name for the XML element representing the rows in the child table.
				string childName = GetChildRelationXElementName(childRelation);

				// Get the value in the parent table.
				DataRow[] childDataRows = dataRow.GetChildRows(childRelation);
				if (childDataRows != null && childDataRows.Length > 0)
				{
					// Create an XML element for the child rows.
					XElement childRowListXElement = new XElement(XName.Get(childName + NAME_EXTENSION_ROWS));
					dataRowXElement.Add(childRowListXElement);

					foreach (DataRow childDataRow in childDataRows)
					{
						// Create an XML element for the current child row.
						XElement childRowXElement = new XElement(XName.Get(childName + NAME_EXTENSION_ROW));
						childRowListXElement.Add(childRowXElement);

						// Fill the 'childRelationXElement'.
						GetXml_FillDataRowXElement(childDataRow, childRowXElement);
					}
				}
			}

			// Retrun.
			return dataRowXElement;
		}


		// GetDataTableOrColumnName
		private string GetDataTableOrColumnName(string name)
		{
			// Replace ' ' with the '_' characters.
			name = name.Replace(' ', '_');

			// Replace '/' with the '_' characters.
			name = name.Replace('/', '_');

			// Return.
			return name.Trim();
		}


		// GetParentRelationXElementName
		private string GetParentRelationXElementName(DataRelation dataRelation)
		{
			string name = dataRelation.ChildColumns[0].ColumnName;
			name = GetDataTableOrColumnName(name);
			name += "_Row";
			return name;
		}


		// GetChildRelationXElementName
		private string GetChildRelationXElementName(DataRelation dataRelation)
		{
			string name = dataRelation.ChildTable.TableName;
			name = GetDataTableOrColumnName(name);
			//name += "_Rows";
			return name;
		}
		#endregion



		#region XSD Methods
		// GetXmlSchema
		public XmlSchema GetXmlSchema()
		{
			XmlSchema xmlSchema = new XmlSchema();

			// Create a schema type for the data table.
			XmlSchemaComplexType dataTableXsdType = new XmlSchemaComplexType();

			// Add the sequence as a compositor type for this XSD element.
			XmlSchemaSequence dataTableXsdSequence = new XmlSchemaSequence();
			dataTableXsdType.Particle = dataTableXsdSequence;

			// Add the data table row XSD element to the sequence.
			XmlSchemaElement dataRowXsdElement = AddXsdElementForDataRow(xmlSchema, _dataTable);
			dataTableXsdSequence.Items.Add(dataRowXsdElement);

			// Add an XSD element for the data table.
			XmlSchemaElement dataTableXsdElement = new XmlSchemaElement();
			xmlSchema.Items.Add(dataTableXsdElement);			// Add it to the sequence element.
			dataTableXsdElement.Name = GetDataTableOrColumnName(_dataTable.TableName) + NAME_EXTENSION_ROWS;
			dataTableXsdElement.SchemaType = dataTableXsdType;

			// Return.
			return xmlSchema;
		}


		// AddXsdTypeForDataTable
		private XmlSchemaElement AddXsdElementForDataRow(XmlSchema xmlSchema, DataTable dataTable)
		{
			// Check if the XSD type for this data table already exist.
			XmlSchemaComplexType dataRowXsdType;
			if (xmlSchema.SchemaTypes.Contains(new System.Xml.XmlQualifiedName(dataTable.TableName)))
				dataRowXsdType = (XmlSchemaComplexType)xmlSchema.SchemaTypes[new System.Xml.XmlQualifiedName(dataTable.TableName)];
			else
			{
				// Create the schema type for the data row.
				dataRowXsdType = new XmlSchemaComplexType();
				dataRowXsdType.Name = GetDataTableOrColumnName(dataTable.TableName);
				xmlSchema.Items.Add(dataRowXsdType);

				// Add the sequence as a compositor type for this XSD element.
				XmlSchemaSequence dataRowXsdSequence = new XmlSchemaSequence();
				dataRowXsdType.Particle = dataRowXsdSequence;


				// Add an XSD attribute to the schema type for each column in the data table.
				foreach (DataColumn dataColumn in dataTable.Columns)
				{
					XmlSchemaAttribute xsdAttribute = new XmlSchemaAttribute();
					dataRowXsdType.Attributes.Add(xsdAttribute);
					xsdAttribute.Name = GetDataTableOrColumnName(dataColumn.ColumnName);
					XmlTypeCode? xmlTypeCode = GetXmlTypeCodeFromNetType(dataColumn.DataType);
					if (xmlTypeCode != null) xsdAttribute.SchemaTypeName = XmlSchemaSimpleType.GetBuiltInSimpleType(xmlTypeCode.Value).QualifiedName;
				}


				// Parent Relations.
				foreach (DataRelation parentRelation in dataTable.ParentRelations)
				{
					// Each relation can be used to build hierarchy for each table only once, otherwise the recursion never stops (stack overflow occures).
					if (_processedDataRelationsInDataTables.ContainsKey(parentRelation.RelationName) &&
						_processedDataRelationsInDataTables[parentRelation.RelationName] != dataTable.TableName) continue;
					_processedDataRelationsInDataTables[parentRelation.RelationName] = dataTable.TableName;

					// Hierarchy is supported only for the data relations with 1 column (parent and child).
					if (parentRelation.ParentColumns.Length > 1 || parentRelation.ChildColumns.Length > 1) continue;

					// Get name for the XML element representing the parent table row.
					string parentName = GetParentRelationXElementName(parentRelation);

					// Add an XSD element to the sequence of the data table row for the parent table.
					DataTable parentDataTable = parentRelation.ParentTable;
					XmlSchemaElement parentDataRowXsdElement = AddXsdElementForDataRow(xmlSchema, parentDataTable);
					dataRowXsdSequence.Items.Add(parentDataRowXsdElement);

					// Fix the name of the element. This element actually represents the navigation property.
					parentDataRowXsdElement.Name = GetParentRelationXElementName(parentRelation);
				}


				// Child Relations.
				foreach (DataRelation childRelation in dataTable.ChildRelations)
				{
					// Each relation can be used to build hierarchy for each table only once, otherwise the recursion never stops (stack overflow occures).
					if (_processedDataRelationsInDataTables.ContainsKey(childRelation.RelationName) &&
						_processedDataRelationsInDataTables[childRelation.RelationName] != dataTable.TableName) continue;
					_processedDataRelationsInDataTables[childRelation.RelationName] = dataTable.TableName;

					// Hierarchy is supported only for the data relations with 1 column (parent and child).
					if (childRelation.ParentColumns.Length > 1 || childRelation.ChildColumns.Length > 1) continue;

					// Get name for the XML element representing the navigation property for the child rows.
					string childName = GetChildRelationXElementName(childRelation) + NAME_EXTENSION_ROWS;

					// Add the type and sequence for the navigation property.
					XmlSchemaComplexType childDataRowListXsdType = new XmlSchemaComplexType();
					XmlSchemaSequence childDataRowListXsdSequence = new XmlSchemaSequence();
					childDataRowListXsdType.Particle = childDataRowListXsdSequence;

					// Add the XSD element representing the data table navigation property.
					XmlSchemaElement childDataRowListXsdElement = new XmlSchemaElement();
					dataRowXsdSequence.Items.Add(childDataRowListXsdElement);			// Add it to the sequence element.
					childDataRowListXsdElement.Name = GetChildRelationXElementName(childRelation) + NAME_EXTENSION_ROWS;
					childDataRowListXsdElement.SchemaType = childDataRowListXsdType;

					// Add XSD element for the child data row.
					XmlSchemaElement childDataRowXsdElement = AddXsdElementForDataRow(xmlSchema, childRelation.ChildTable);
					childDataRowListXsdSequence.Items.Add(childDataRowXsdElement);
				}
			}


			// Add the XSD element representing the data table row.
			XmlSchemaElement dataRowXsdElement = new XmlSchemaElement();
			dataRowXsdElement.Name = GetDataTableOrColumnName(dataTable.TableName) + NAME_EXTENSION_ROW;
			dataRowXsdElement.SchemaTypeName = new System.Xml.XmlQualifiedName(dataRowXsdType.Name);


			// Return.
			return dataRowXsdElement;
		}


		// GetXmlTypeCodeFromNetType
		private XmlTypeCode? GetXmlTypeCodeFromNetType(Type type)
		{
			switch (Type.GetTypeCode(type))
			{
				case TypeCode.Boolean:
					return XmlTypeCode.Boolean;
				case TypeCode.Char:
					return XmlTypeCode.String;
				case TypeCode.SByte:
					return XmlTypeCode.Byte;
				case TypeCode.Byte:
					return XmlTypeCode.UnsignedByte;
				case TypeCode.Int16:
					return XmlTypeCode.Short;
				case TypeCode.UInt16:
					return XmlTypeCode.UnsignedShort;
				case TypeCode.Int32:
					return XmlTypeCode.Integer;
				case TypeCode.UInt32:
					return XmlTypeCode.UnsignedInt;
				case TypeCode.Int64:
					return XmlTypeCode.Long;
				case TypeCode.UInt64:
					return XmlTypeCode.UnsignedLong;
				case TypeCode.Single:
					return XmlTypeCode.Float;
				case TypeCode.Double:
					return XmlTypeCode.Double;
				case TypeCode.Decimal:
					return XmlTypeCode.Decimal;
				case TypeCode.DateTime:
					return XmlTypeCode.DateTime;
				case TypeCode.String:
					return XmlTypeCode.String;
				default:
					return null;
			}
		}
		#endregion
	}
}
